漏洞详情
Spring Messaging 属于Spring Framework项目,其定义了Enterprise Integration Patterns典型实现的接口及相关的支持(注解,接口的简单默认实现等)
https://pivotal.io/security/cve-2018-1270
影响范围
1 | Spring |
Spring Framework允许应用程序通过spring-messaging模块通过简单的内存STOMP代理通过WebSocket端点公开STOMP。恶意用户(或攻击者)可以向代理发送消息,这可能导致远程执行代码攻击。
复现环境
https://repo.spring.io/release/org/springframework/spring/5.0.0.RELEASE/
SPEL
spel是Spring Expression Language 即,spring表达式语言,是一个支持查询和操作运行时对象导航图功能的强大的表达式语言.支持以下功能1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19文字表达式
布尔和关系运算符
正则表达式
类表达式
访问 properties, arrays, lists, maps
方法调用
关系运算符
参数
调用构造函数
Bean引用
构造Array
内嵌lists
内嵌maps
三元运算符
变量
用户定义的函数
集合投影
集合筛选
模板表达式
一个hello world1
2
3
4
5
6
7
8
9
10
11
12import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
public class Spel {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
String var1 = (String)parser.parseExpression("'Hello World'").getValue();
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();
System.out.println(maxValue);
System.out.println(var1);
}
}

弹calc
代码1
String calc = (String)parser.parseExpression("T(Runtime).getRuntime().exec('open /Applications/Calculator.app/')").getValue();

这里的T是类型操作符,可以从类路径加载指定类名称(全限定名)所对应的 Class 的实例,格式为:T(全限定类名),效果等同于 ClassLoader#loadClass()
Websocket
由于HTTP具有单向通信的特点,于是造成了Server向Client推送消息变得很难,需要使用轮询的方式,于是有了WebSocket,他支持双向通信,类似于聊天室模式,在这个会话里,Server和Clinet都能发送数据,相互通信,所以WebSocket是一种在一个TCP连接上能够全双工,双向通信的协议
Stomp
STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,一个非常简单和容易实现的协议,提供了可互操作的连接格式,易于开发并应用广泛。这个协议可以有多种载体,可以通过HTTP,也可以通过WebSocket。在Spring-Message中使用的是STOMP Over WebSocket。
STOMP是一种基于帧的协议, STOMP是基于Text的,但也允许传输二进制数据。 它的默认编码是UTF-8,但它的消息体也支持其他编码方式,比如压缩编码。
一个STOMP帧由三部分组成:命令,Header(头信息),Body(消息体)
1 | COMMAND |
一个实际帧结构1
2
3
4
5SEND
destination:/broker/roomId/1
content-length:57
{“type":"ENTER","content":"o7jD64gNifq-wq-C13Q5CRisJx5E"}
spring 的消息功能是基于消息代理构建的
demo
stomp的架构图
图中各个组件介绍:
- 生产者型客户端(左上组件): 发送SEND命令到某个目的地址(destination)的客户端。
- 消费者型客户端(左下组件): 订阅某个目的地址(destination), 并接收此目的地址所推送过来的消息的客户端。
- request channel: 一组用来接收生产者型客户端所推送过来的消息的线程池。
- response channel: 一组用来推送消息给消费者型客户端的线程池。
- broker: 消息队列管理者,也可以成为消息代理。它有自己的地址(例如“/topic”),客户端可以向其发送订阅指令,它会记录哪些订阅了这个目的地址(destination)。
- 应用目的地址(图中的”/app”): 发送到这类目的地址的消息在到达broker之前,会先路由到由应用写的某个方法。相当于对进入broker的消息进行一次拦截,目的是针对消息做一些业务处理。
- 非应用目的地址(图中的”/topic”,也是消息代理地址): 发送到这类目的地址的消息会直接转到broker。不会被应用拦截。
- SimpAnnotatonMethod: 发送到应用目的地址的消息在到达broker之前, 先路由到的方法. 这部分代码是由应用控制的。
1 | git clone https://github.com/spring-guides/gs-messaging-stomp-websocket |
几个重要的文件
GreetingController.java(相当于图中的SimpAnnotatonMethod)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20package hello;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
public class GreetingController {
//使用MessageMapping注解来标识所有发送到“/hello”这个destination的消息,都会被路由到这个方法进行处理
("/hello")
//使用SendTo注解来标识这个方法返回的结果,都会被发送到它指定的destination,“/topic/greetings”.
("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Thread.sleep(1000); // simulated delay
return new Greeting("Hello, " + message.getName() + "!");
}
}
尤其注意,这个处理器方法有一个返回值,这个返回值并不是返回给客户端的,而是转发给消息代理的,如果客户端想要这个返回值的话,只能从消息代理订阅
WebSocketConfig.java1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
//使用Configuration注解标识这是一个Springboot的配置类.
//使用此注解来标识使能WebSocket的broker.即使用broker来处理消息
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
public void configureMessageBroker(MessageBrokerRegistry config) {
//应用程序以 /app 为前缀,而 代理目的地以 /topic 为前缀
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket").withSockJS();
}
}
运行后
漏洞复现
在static/app.js
添加header,需要指定为selector,在文档里有说明,是一个选择器
https://stomp.github.io/stomp-specification-1.0.html

随便发送一条消息即可触发

可以抓个包看到websocket发送的STOMP帧
点击connect时

点击send时
漏洞分析
SUBSCRIBE
先在ExecutorSubscribableChannel.class的run函数下断,可以发现先来的第一条命令是CONNECT
然后是SUBSCRIBE命令,此时的message如下
然后从header取出selector并判断是否为null
不为null后,调用expressionParser.parseExpression处理selector,返回给expression变量,
然后调用subscriptionRegistry.addSubscription方法添加订阅
传入sessionId,subsId,destination和expression,首先从sessionid中获得info,如果没有就注册订阅,存储着session里的东西,返回DefaultSubscriptionRegistry的实例info
然后获取value,这里为null1
DefaultSubscriptionRegistry.SessionSubscriptionInfo value = (DefaultSubscriptionRegistry.SessionSubscriptionInfo)this.sessions.putIfAbsent(sessionId, info);
往下走,添加订阅
把destination put 进destinationLookup
然后调用Subscription初始化id和selector值,最后add进subs变量


然后在缓存中更新订阅
Send
发送的message为
前面的调用很多,我们之间从处理的地方开始跟进
在这之前的调用栈,通过反射来调到我们请求的方法
inovke处理完后,会返回我们的控制器里的值
往下执行,跟入handleReturnValue
调用getReturnValueHandler来获得handler变量,用于处理return数据,有默认的defaultDestinationPrefix

继续跟进
提取headers等信息

然后get Sessionid 为c01pvxeq
获取destination
进入for循环后,首先调用createHeaders,把sessionid传入,主要是设置session和header
然后跟到convertAndSend
调用doConvert函数得到message,然后调用send,开始发送message

调用sendInternal
message
由于timeout为-1,所以调用send传入message参数

跟入sendInternal



调用SimpleBrokerMessageHandler的handleMessageInternal
获取到信息后调用updateSessionReadTime更新时间
检测是否是perfix开头,这里发往的订阅目的地是/topic/greetings,所以checkDestinationPrefix为true
调用sendMessageToSubscribers发送message到订阅地址
跟入findSubscriptions查找订阅,检测destination,没有error则进入findSubscriptionsInternal函数

这里的destinationCache变量是
还记得一开始处理订阅的时候么,我们有一些put操作,相当于存入缓存了,这里就是一个提取的操作
调用DefaultSubscriptionRegistry的getSubscriptions获取订阅信息,
从整个订阅里获取全部的信息然后保存到一个迭代器里,并且赋值给info

最后根据sessionID add result里
最后更新缓存
然后跟进filterSubscriptions
首先判断selector是否存在,存在则进入判断result变量传入后形参是allMatches
提取出expression,判断不为空
然后在getValue执行spel表达式
调用链很长
修复
更改为要重用的静态计算上下文,SimpleEvaluationContext 对于权限的限制更为严格,能够进行的操作更少。只支持一些简单的Map结构。
来看看修改为SimpleEvaluationContext如下代码的执行情况1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
public class Spel {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression calc = parser.parseExpression("T(Runtime).getRuntime().exec('open /Applications/Calculator.app/')");
calc.getValue(context);
}
}

不再弹出calc。
Reference
- https://my.oschina.net/genghz/blog/1796965
- https://www.toptal.com/java/stomp-spring-boot-websocket
- https://pivotal.io/security/cve-2018-1270
- https://juejin.im/post/5b7071ade51d45665816f8c0
- https://juejin.im/post/5b9cb6825188255c5546e5f4
- https://spring.io/guides/gs/messaging-jms/
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.html
- https://blog.csdn.net/pacosonswjtu/article/details/51914567
- https://www.cnblogs.com/jmcui/p/8999998.html


